package com.example.dbsample.util.string;

import lombok.extern.slf4j.Slf4j;
import sun.reflect.ConstructorAccessor;
import sun.reflect.FieldAccessor;
import sun.reflect.ReflectionFactory;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Slf4j
public class DynamicEnumUtil {
    private static ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();

    public static void addEnum(Class enumType, String enumName, Class[] additionalType, Object[] additionalValues) {
        if(!Enum.class.isAssignableFrom(enumType)) {
            throw new RuntimeException("class " + enumType + " is not an instant of Enum");
        }

        // 1. Lookup "$VALUES" holder in enum class and get previous enum instances
        Field valuesField = null;

        Field[] fields = enumType.getDeclaredFields();

        for(Field field : fields) {
            if(field.getName().contains("$VALUES")) {
                valuesField = field;
                break;
            }
        }

        AccessibleObject.setAccessible(new Field[]{valuesField}, true);

        try {
            // 2. copy it
            Object[] previousValues = (Object[]) valuesField.get(enumType);
            List values = new ArrayList(Arrays.asList(previousValues));

            // 3. build new enum
            Object newValue = (Object) makeEnum(enumType, enumName, values.size(), additionalType, additionalValues);

            // 4. add new value
            values.add(newValue);

            // 5. set new values field
            setFailsafeFieldValue(valuesField, null, values.toArray((Object[]) Array.newInstance(enumType, 0)));

            // 6. clean enum cache
            cleanEnumCache(enumType);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    private static void setFailsafeFieldValue(Field field, Object target, Object value)
            throws NoSuchFieldException, IllegalAccessException {

        // let's make the field accessible
        field.setAccessible(true);

        // next we change the modifier in the Field instance to
        // not be final anymore, thus tricking reflection into
        // letting us modify the static final field
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        int modifiers = modifiersField.getInt(field);

        // blank out the final bit in the modifiers int
        modifiers &= ~Modifier.FINAL;
        modifiersField.setInt(field, modifiers);

        FieldAccessor fa = reflectionFactory.newFieldAccessor(field, false);
        fa.set(target, value);

    }

    private static void blankField(Class<?> enumClass, String fieldName)
            throws NoSuchFieldException, IllegalAccessException {
        for (Field field : Class.class.getDeclaredFields()) {
            if (field.getName().contains(fieldName)) {
                AccessibleObject.setAccessible(new Field[]{field}, true);
                setFailsafeFieldValue(field, enumClass, null);
                break;
            }
        }
    }

    private static void cleanEnumCache(Class<?> enumClass)
            throws NoSuchFieldException, IllegalAccessException {
        blankField(enumClass, "enumConstantDirectory"); // Sun (Oracle?!?) JDK 1.5/6
        blankField(enumClass, "enumConstants"); // IBM JDK
    }

    private static ConstructorAccessor getConstructorAccessor(
            Class<?> enumClass, Class<?>[] additionalParameterTypes) throws NoSuchMethodException {
        Class<?>[] parameterTypes = new Class[additionalParameterTypes.length + 2];
        parameterTypes[0] = String.class;
        parameterTypes[1] = int.class;
        System.arraycopy(
                additionalParameterTypes, 0, parameterTypes, 2, additionalParameterTypes.length);
        return reflectionFactory.newConstructorAccessor(
                enumClass.getDeclaredConstructor(parameterTypes));
    }
    private static Object makeEnum(
            Class<?> enumClass,
            String value,
            int ordinal,
            Class<?>[] additionalTypes,
            Object[] additionalValues)
            throws Exception {
        Object[] parms = new Object[additionalValues.length + 2];
        parms[0] = value;
        parms[1] = Integer.valueOf(ordinal);
        System.arraycopy(additionalValues, 0, parms, 2, additionalValues.length);
        //       parms[1] = parms[parms.length-1];
        return enumClass.cast(getConstructorAccessor(enumClass, additionalTypes).newInstance(parms));
    }

    /**
     * 判断枚举是否已存在
     * @param values
     * @param enumName
     * @param <T>
     * @return
     */
    public static <T extends Enum<?>> boolean contains(List<T> values, String enumName){
        for (T value : values) {
            if (value.name().equals(enumName)) {
                return true;
            }
        }
        return false;
    }
}
